package jflowmap.model.ontology;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Vector;

import com.google.common.collect.Sets;

public class Aggregation {

	private boolean finished = false;
	
	private final List<Aggregation> dependencies = new Vector<Aggregation>();
	private final Context context;
	private final Map<Dimension, Map<Match, List<Value>>> aggregates =
			new HashMap<Dimension, Map<Match, List<Value>>>();
	
	private List<List<Match>> matches = new Vector<List<Match>>();
	
	private Aggregator aggregator;
	
	public Aggregation enables (Aggregation other) {
		if (!other.dependencies.contains(this)) {
			other.dependencies.add(this);
		}
		return this;
	}
	
	public Aggregation needs (Aggregation other) {
		if (!dependencies.contains(other)) {
			dependencies.add(other);
		}
		return this;
	}
	
	public void reset() {
		finished = false;
	}
	
	public boolean hasFinished() {
		return finished;
	}
	
	public Aggregation with (Aggregator aggregator) {
		this.aggregator = aggregator;
		
		Set<Match> constraints = new HashSet<Match>();
					
		if (matches.size() == 1) {
			constraints.addAll(matches.get(0));
			matches.clear();
			
		} else {
			constraints.add(new Match());
			for (List<Match> sub: matches) {
				constraints = combine(constraints, sub);
			}
		}

		for (Map<Match, List<Value>> eachAggregate: aggregates.values()) {
			for (Match constraint: constraints) {
				eachAggregate.put(constraint, new Vector<Value>());
			}
		}
		
		return this;
	}
	
	public void aggregate() {
		if (finished) return;
		for (Aggregation dependency: dependencies) {
			if (!dependency.finished) {
				System.out.println("Aggregation dependends on another. Calculating other first...");
				dependency.aggregate();
				System.out.println("-- OK.");
			}
		}
		
		final Set<Dimension> aggregateDimensions = aggregates.keySet();
		
		for (final Entry<Dimension, Map<Match, List<Value>>> entry: aggregates.entrySet()) {
			Set<Dimension> childrenOrParts = null;

			if (this.collect == Collect.EACH_CHILD) {
				childrenOrParts = entry.getKey().children();
			} else {
				childrenOrParts = entry.getKey().parts();
			}
			
			for (Dimension childOrPart: childrenOrParts) {
				Set<Dimension> requirement = Sets.union(Sets.newHashSet(childOrPart),
						entry.getValue().keySet().iterator().next().getDimensions());
				context.require(requirement);	
			}
		}
		
		for (Value value: context.getValues()) {
			
			Dimension d = null;
			if (this.collect == Collect.EACH_CHILD) {
				d = value.childOfAny(aggregateDimensions);
			} else if (this.collect == Collect.EACH_PART) {
				d = value.partOfAny(aggregateDimensions);
			}
			if (d == null) continue;
			
			final Set<Match> matches = aggregates.get(d).keySet();
			for (Match match: matches) {
				if (match.matches(value)) {
					aggregates.get(d).get(match).add(value);
				}
			}
		}
		
		for (final Entry<Dimension, Map<Match, List<Value>>> entry: aggregates.entrySet()) {
			for (final Entry<Match, List<Value>> matchingEntry: entry.getValue().entrySet()) {
							
				final Value aggregateValue = aggregator.aggregate(matchingEntry.getValue(), context);
				for (Dimension dim: matchingEntry.getKey().getDimensions()) {
					aggregateValue.as(dim);
				}
				aggregateValue.as(entry.getKey());
			}
		}
		
		finished = true;
	}
	
	private Set<Match> combine (Set<Match> matches, List<Match> additionalMatches) {
		Set<Match> combinedMatches = new HashSet<Match>();
		for (Match match: matches) {
			for (Match additionalMatch: additionalMatches) {
				final Match combined = new Match();
				combined.add(match.getDimensions());
				combined.add(additionalMatch.getDimensions());
				combinedMatches.add(combined);
			}
		}
		return combinedMatches;
	}
	
	
	public Aggregation aggregateAllValuesChildrenOf (String dimension) {
		return aggregateAllValuesChildrenOf(Sets.newHashSet(context.getDimension(dimension)));
	}
	
	public Aggregation aggregateAllValuesPartsOf (String dimension) {
		return aggregateAllValuesPartsOf(Sets.newHashSet(context.getDimension(dimension)));
	}

	public Aggregation forEvery (String dimension) {
		return forEvery(Sets.newHashSet(context.getDimension(dimension)));
	}
	
	public Aggregation aggregateAllValuesChildrenOf (DimensionCollection collection) {
		return aggregateAllValuesChildrenOf(collection.get());
	}
	
	public Aggregation aggregateAllValuesPartsOf (DimensionCollection collection) {
		return aggregateAllValuesPartsOf(collection.get());
	}

	public Aggregation forEvery (DimensionCollection collection) {
		return forEvery(collection.get());
	}
	
	public Aggregation aggregateAllValuesChildrenOf (Set<Dimension> dimensions) {
		for (Dimension d: dimensions) {
			this.aggregates.put(d, new HashMap<Match, List<Value>>());
		}
		this.collect = Collect.EACH_CHILD;
		return this;
	}
	
	public Aggregation aggregateAllValuesPartsOf (Set<Dimension> dimensions) {
		for (Dimension d: dimensions) {
			this.aggregates.put(d, new HashMap<Match, List<Value>>());
		}
		this.collect = Collect.EACH_PART;
		return this;
	}
			
	public Aggregation forEvery (Set<Dimension> constraints) {
		final List<Match> tmp = new Vector<Match>();
		for (Dimension d: constraints) {
			tmp.add(new Match(d));
		}
		matches.add(tmp);
		return this;
	}
	
	public boolean provides (Set<Dimension> dimensions) {
		for (final Entry<Dimension, Map<Match, List<Value>>> entry: aggregates.entrySet()) {
			final Set<Dimension> without = Sets.difference(dimensions, Sets.newHashSet(entry.getKey()));
			
			if (without.size()+1 == dimensions.size()) {
				for (Match match: entry.getValue().keySet()) {
					if (match.getDimensions().equals(without)) {
						return true;
					}
				}
			}
		}

		return false;
		/*		 
		if (aggregates.containsKey(parentDimension)) {
			for (Dimension d: dimensions) {
				boolean found = false;
				for (final List<Match> matches: this.matches) {
					for (Match match: matches) {
						if (match.getDimensions().contains(d)) {
							found = true;
							break;
						}
					}
					if (found) break;
				}
				if (!found) return false;
			}
		}
		}
		
		return true;*/
	}
	
	Aggregation (final Context context) {
		this.context = context;
	}
	
	
	private enum Collect {
		EACH_CHILD, EACH_PART
	};
	
	private Collect collect = Collect.EACH_CHILD;

	@Override
	public String toString() {
		return "Aggregation [\naggregates=" + aggregates
				+ "\nmatches=" + matches + "\naggregator="
				+ aggregator + "\ncollect=" + collect
				+ "\n]";
	}

	public Aggregation aggregateAllValuesChildrenOf(Dimension parentDimension) {
		return this.aggregateAllValuesChildrenOf(Sets.newHashSet(parentDimension));
	} 
}
